package socket; 
 /*
 @Time: 2022/7/16 14:06
 @Author:GShuai
 @File:Server.class
 @Software:IntelliJ IDEA
*/

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;

/**
 * 聊天室服务端
 */
public class Server {
    /*
        java.net.ServerSocket
        运行在服务端上的，主要有两个职责：
        1：向系统申请服务端口，客户端就是通过该端口与服务器建立连接的
        2:监听该端口，一旦一个客户端建立连接，就会立即返回一个Socket实例，使用它就可以
          与该客户端交互了。
        ServerSocket比作"总机"
     */
    private ServerSocket serverSocket;

    /*
        该数组里存放所有给所有客户端发消息的输出流，用于广播消息.
     */
//    private PrintWriter[] allOut = {};
    private Collection<PrintWriter> allOut = new ArrayList<>();

    public Server() {
        try {
            System.out.println("正在启动服务端...");
            /*
              实例化ServerSocket的同时指定服务端口，客户端就可以用过该端口与服务器建立连接。
              注意，该端口不能与服务器计算机上其他的程序申请的端口一致，否则会抛出
              异常：java.net.BindException:address already in use  即：地址被占用异常
              解决办法：换一个端口，或者将占用8088端口的程序进程杀死。

              1:ctrl+ alt +del 选择任务管理器
              2：在任务管理器窗口中点击【进程】
              3：在进程中，找到IDEA进程，将其展开，杀死所有Java(TM)开头的进程即可

              端口的选择：范围是0-65535之间的整数。但是实际开发中前6000不建议选取
              因为密集绑定了系统应用和流行的应用程序。建议8000以后的端口。
             */
            serverSocket = new ServerSocket(8088);
            System.out.println("服务端启动完毕！");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void start() {
        try {
            while (true) {
                System.out.println("等待客户端连接 .....");
            /*
                ServerSocket的方法：
                Socket accept()
                该方法是一个阻塞方法，调用后开始等待客户端的连接，一旦一个客户端连接了
                accept方法会立即返回一个Socket实例，通过这个Socket可以与连接的客户端
                进行交互。
                多次调用accept可以接受多个客户端的连接、理解为是总机"接电话"的操作。

             */
                Socket socket = serverSocket.accept();
                System.out.println("一个客户端连接了！");

                //启动一个线程来处理与该客户端的交互
                ClientHandler clientHandler = new ClientHandler(socket);
                Thread t = new Thread(clientHandler);
                t.start();


            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


    public static void main(String[] args) {
        Server server = new Server();
        server.start();
    }

    /**
     * 该线程任务负责与指定客户端交互
     */
    private class ClientHandler implements Runnable {
        private Socket socket;
        private String host; // 记录客户的地址信息

        public ClientHandler(Socket socket) {
            this.socket = socket;
            // 通过socket获取远端计算机地址信息
            host = socket.getInetAddress().getHostAddress();
        }

        public void run() {
            PrintWriter pw = null;
            try {
             /*
                通过socket的方法
                InputStream  getInputStream()
                获取一个字节输入流，来读取远端计算机发送过来的字节
             */
                InputStream in = socket.getInputStream();
                InputStreamReader isr = new InputStreamReader(in, StandardCharsets.UTF_8);
                BufferedReader br = new BufferedReader(isr);

                /*
                    通过socket获取输出流，用于将消息发送给客户端
                 */
                OutputStream out = socket.getOutputStream();
                OutputStreamWriter osw = new OutputStreamWriter(out, StandardCharsets.UTF_8);
                BufferedWriter bw = new BufferedWriter(osw);
                pw = new PrintWriter(bw, true);

                /*
                    this不可以，因为这里的this是ClientHandler的实例，而每个线程都有自己的任务（不同的
                    ClientHandler实例，因此多个线程看到的ClientHandler并非同一个对象。）
                 */
//                  synchronized (this) {
                /*
                 通常情况下，同步监视器对象可以选择临界资源本身，即：多个 线程抢谁，就锁谁。

                 虽然本案例中allOut是临界资源，但是这里不能使用它作为同步监视器对象，主要原因是：
                 同步块中有对该数组扩容这类操作，这会导致allOut对象在一直发生改变。
                 */


//                synchronized (Server.this) {
//                    //将对应该客户端的输出流存入allOut数组,便于其他ClientHandler 广播消息给当前客户端
//                    // 1.对allOut数组扩容
//                    allOut = Arrays.copyOf(allOut, allOut.length + 1);
//                    // 2.将输出流存入数组最后一个位置
//                    allOut[allOut.length - 1] = pw;
//                }

                // 同步监视器对象可以直接使用allOUt，因为增删元素集合对象本身没变
                synchronized (allOut){
                    allOut.add(pw);
                }

                // 广播该客户端上线了
//                sendMessage(host+"上线了,当前现在人数:"+allOut.length);
                sendMessage(host+"上线了,当前现在人数:"+allOut.size());

                String message;
             /*
                使用缓冲字符输入流读取客户端发送过来一行字符串的操作，可能会因为客户端
                的异常断开而抛出异常:
                java.net.SocketException: Connection reset
             */
                while ((message = br.readLine()) != null) {
                    System.out.println(host + "说:" + message);
                    //遍历allOut数组中所有输出流,将消息发送给所有客户端
                    sendMessage(host + "说:" + message);   // 直接调用即可
                }

            } catch (IOException e) {

            }finally {
                //处理该客户端断开连接后的操作

                // 将对应当前客户端的输出流从共享数组allOut中删除
//                synchronized (Server.this) {
//                    for (int i = 0; i < allOut.length; i++) {
//                        if (allOut[i] == pw) {
//                            allOut[i] = allOut[allOut.length - 1];
//                            allOut = Arrays.copyOf(allOut, allOut.length - 1);
//                            break;
//                        }
//                    }
//                }

                synchronized (allOut){
                    allOut.remove(pw);
                }



                // 广播该客户端下线了
//                sendMessage(host+"下线了,当前现在人数:"+allOut.length);
                sendMessage(host+"下线了,当前现在人数:"+allOut.size());


                try {
                    // 关闭socket,释放底层资源
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        /**
         *  广播消息给所有客户端
         * @param message
         */
        public void sendMessage(String message){
            //这里添加synchronized的目的是为了和上面的增删数组元素互斥，避免遍历出错
//            synchronized (Server.this) {
//                for (int i = 0; i < allOut.length; i++) {
//                    allOut[i].println(message);
//                }
//            }

            synchronized (allOut) {
                for (PrintWriter pw :allOut) {
                    pw.println(message);
                }
            }
        }
    }
}
